#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <dlfcn.h>
#include <sys/time.h>
#include <execinfo.h>
#include "dl.h"

pthread_mutex_t mutex;
struct timeval g_base_time;
bool started = false;

thread_local bool thread_rec_created = false;
thread_local class thread_rec_t *thread_rec;
extern class thread_rec_t g_thread_rec;

thread_local class thread_rec_t *rec_poll;
thread_local int poll_index = 0;
#define POLL_SIZE 80000000

class thread_rec_t : public  dl_t {
public:
    int pid;
    bool enter;
    int indent;
    void *fnp;
    void *caller;
    char fname[256];
    struct timeval time;
    struct timeval real_time;
    unsigned long count;
public:
    thread_rec_t(): dl_t() {
        pid = (int)syscall(SYS_gettid);
        if (thread_rec_created == false) {
            thread_rec_created = true;
            thread_rec = new thread_rec_t;
            pthread_mutex_lock(&mutex);
            g_thread_rec.insert(thread_rec, NULL);
            pthread_mutex_unlock(&mutex);
            rec_poll = (thread_rec_t *)malloc(sizeof(thread_rec_t) * POLL_SIZE);
        }
    }
    void run(){};
};

thread_local thread_rec_t _thread_rec;
thread_rec_t g_thread_rec;

char *cared_func = NULL;
__thread bool recording = false;
int max_indent = 0;
void __attribute__((constructor))
tracker_init()
{
    char *indent = getenv("TRACKER_MAX_INDENT");
    if (indent) {
        max_indent = atoi(indent);
    }
    cared_func = getenv("TRACKER_CARED_FUNC");
    pthread_mutex_init(&mutex, 0);
    char *symbol_file = getenv("SYMBOL_FILE");
    if (symbol_file == NULL) return;
}

struct func_stat_t : public dl_t {
    void *fnp;
    char fn[1024];
    int count;
    size_t total_dur;
    size_t max_dur;
    size_t min_dur;
    void new_fnp(char *fn, void *fnp, size_t dur) {
        func_stat_t *n = NULL;
        for (func_stat_t *p = (func_stat_t *)head;p;p = (func_stat_t *)p->next) {
            if (p->fnp == fnp) {
                n = p;
                break;
            }
        }
        if (!n) {
            n = new func_stat_t;
            n->fnp = fnp;
            if (fn) {
                strcpy(n->fn, fn);
            } else {
                sprintf(n->fn, "%p", fnp);
            }
            n->count = 1;
            n->total_dur = dur;
            n->max_dur = dur;
            n->min_dur = dur;
            insert(n, NULL);
        } else {
            n->count++;
            n->total_dur += dur;
            if (dur > n->max_dur) n->max_dur = dur;
            if (dur < n->min_dur) n->min_dur = dur;
        }
    }
    void destory() {
        while (head) {
            func_stat_t *p = (func_stat_t *)head;
            head = (func_stat_t *)head->next;
            delete p;
        }
        tail = NULL;
    }
    void show_all(FILE *f) {
        fprintf(f, "echo \"func_name\tcount\ttotal_dur(us)\tmax_dur(us)\tmin_dur(us)\"\n");
        for (func_stat_t *p = (func_stat_t *)head;p;p = (func_stat_t *)p->next) {
            fprintf(f, "t=$(%s);echo \"${t:42:-1}\"\t%p\"\t%d\t%zu\t%zu\t%zu\"\n", p->fn, p->fnp, p->count, p->total_dur, p->max_dur, p->min_dur);
        }
    }
};

void *hextop(char *s)
{
    unsigned long ret = 0;
    while (s && *s != ' ' && *s != 0 && *s != '\n') {
        char c = *s;
        if (c >= '0' && c <= '9') {
            ret <<=4;
            ret += c - '0';
        } else if (c >= 'a' && c <= 'f') {
            ret <<=4;
            ret += c + 10 - 'a';
        } else if (c >= 'A' && c <= 'F') {
            ret <<=4;
            ret += c + 10 - 'A';
        }
        s++;
    }
    return (void *)ret;
}

size_t show_rec(FILE *f, thread_rec_t *&rec, func_stat_t &fstat, int indent)
{
    size_t sub_dur = 0;
    void *fnp = NULL;
    thread_rec_t *e = NULL;
    while (rec) {
        char *fname = rec->fname;
        fprintf(f, "t=$(%s);echo \"", rec->fname);
        if (rec->enter) {
            fnp  = rec->fnp;
            e = rec;
            for (int i = 0;i < indent;i++) {
                fprintf(f, "    ");
            }
        } else {
            if (rec->fnp != fnp) {
                printf("Error %d\n",  __LINE__);
            }
            for (int i = 0;i < indent;i++) {
                fprintf(f, "    ");
            }
            fprintf(f, "} /* ");
        }

        fprintf(f, "${t:42:-1} %p ", rec->fnp);

        if (rec->enter) {
            size_t dur = rec->time.tv_sec - g_base_time.tv_sec;
            dur = dur * 1000000 + rec->time.tv_usec - g_base_time.tv_usec;
            Dl_info info;
            dladdr(rec->caller, &info);
            fprintf(f, "%zu(us): {\"\n", dur);
            rec = (thread_rec_t *)rec->next;
            while (rec) {
                if (rec && rec->enter) {
#if 0
                    dladdr(rec->caller, &info);
                    if (info.dli_fname != caller_module1 && info.dli_fname != caller_module2) {
                        int level = 0;
                        do  {
                            if (rec->enter) level++;
                            else level--;
                            rec = (thread_rec_t *)rec->next;
                        } while (rec && level > 0);
                        continue;
                    }
#endif
                    sub_dur += show_rec(f, rec, fstat, indent + 1);
                } else {
                    break;
                }
            }
        } else {
            size_t ret_dur = rec->time.tv_sec - e->time.tv_sec;
            ret_dur = ret_dur * 1000000 + rec->time.tv_usec - e->time.tv_usec;
            size_t dur = rec->real_time.tv_sec - e->real_time.tv_sec;
            dur = dur * 1000000 + rec->real_time.tv_usec - e->real_time.tv_usec;
            fprintf(f, "%zu(us) ", dur);
            if (dur > sub_dur) dur -= sub_dur;
            else dur = 0;
            fstat.new_fnp(fname, rec->fnp, dur);
            if (rec->count > 1) {
                fprintf(f, "calls: %lu ", rec->count);
            }
            fprintf(f, "*/\"\n");
            rec = (thread_rec_t *)rec->next;
            return ret_dur;
        }
    }
    return sub_dur;
}

int tracker_retry = 0;
void
tracker_destroy()
{
    if (poll_index >= POLL_SIZE) {
        printf("poll_index = %d\n", ++poll_index);
        return;
    }
    char s[1024];
    for (thread_rec_t *rec_list = (thread_rec_t *)g_thread_rec.head;rec_list;rec_list = (thread_rec_t *)rec_list->next) {
        if (rec_list->head == NULL) continue;
        sprintf(s, "/home/szx/tracker/out%d_%d.sh", getpid(), rec_list->pid);
        FILE *f = fopen(s, "w");
        func_stat_t fstat;
        fprintf(f, "#!/bin/bash\n");
        fprintf(f, "echo \"-------------------------------\"\n");
        fprintf(f, "echo \"pid: %d\"\n", rec_list->pid);
        thread_rec_t *rec = (thread_rec_t *)rec_list->head;
        while (rec) show_rec(f, rec, fstat, 0);
        if (rec_list->head) {
            size_t dur = ((thread_rec_t *)rec_list->tail)->time.tv_sec - ((thread_rec_t *)rec_list->head)->time.tv_sec;
            dur = dur * 1000000 + ((thread_rec_t *)rec_list->tail)->time.tv_usec - ((thread_rec_t *)rec_list->head)->time.tv_usec;
            fstat.show_all(f);
            fstat.destory();
            fprintf(f, "========== during %zu(us)\n", dur);
        }
        fclose(f);
    }
}

extern "C" {
bool tracke_enable = false;
__thread bool tracke_enable_thread = false;
__thread int exit_tracker = 0;
__thread int t_indent = 0;

void enable_tracker()
{
    if (started == false) {
        gettimeofday(&g_base_time, NULL);
        started = true;
    }
    tracke_enable = true;
}

void disable_tracker()
{
    struct timeval now;
    gettimeofday(&now, NULL);
    size_t dur = now.tv_sec - g_base_time.tv_sec;
    dur = dur * 1000000 + now.tv_usec - g_base_time.tv_usec;
    printf("running time: %zu(us)\n", dur);
    bool show_result = tracke_enable;
    tracke_enable = false;
    if (show_result) {
        tracker_destroy();
    }
}

void enable_tracker_t()
{
    if (started == false) {
        gettimeofday(&g_base_time, NULL);
        started = true;
    }
    tracke_enable_thread = true;
}

void disable_tracker_t()
{
    bool show_result = tracke_enable_thread;
    tracke_enable_thread = false;
    if (show_result) {
        tracker_destroy();
    }
}

void get_fname_fast(void *fp, char *fname)
{
    Dl_info info;
    memset(&info, 0x00, sizeof(Dl_info));
    int r = dladdr(fp, &info);
    if (info.dli_sname && r == 0) {
        strcpy(fname, info.dli_fname);
    } else {
        sprintf(fname, "echo \"disassemble 0x%lx\" | gdb %s | grep \"Dump of assembler code for function\"", ((unsigned long)fp) - (unsigned long)info.dli_fbase, info.dli_fname);
    } 
}

void __attribute__((no_instrument_function))
tracker_cyg_profile_func_enter(void *this_func, void *call_func)
{
    _thread_rec.run();
    if (!rec_poll) return;
    if (tracke_enable == false && tracke_enable_thread == false && exit_tracker == 0) return;
    if (recording == true) return;
    struct timeval time;
    gettimeofday(&time, NULL);
    t_indent++;
    exit_tracker++;
    if (max_indent != 0 && t_indent > max_indent) {
        return;
    }
    recording = true;

    if (poll_index < POLL_SIZE) {
        thread_rec_t *rec = &rec_poll[poll_index++];
        rec->enter = true;
        rec->fnp = this_func;
        rec->caller = call_func;
        get_fname_fast(this_func, rec->fname);
        rec->time = time;
        thread_rec->insert(rec, NULL);
        gettimeofday(&rec->real_time, NULL);
    } else {
        //printf("poll_index = %d\n", ++poll_index);
    }
    recording = false;
}

void __attribute__((no_instrument_function))
tracker_cyg_profile_func_exit(void *this_func, void *call_func)
{
    struct timeval time;
    gettimeofday(&time, NULL);
    if (exit_tracker == 0) return;
    if (recording == true) return;
    t_indent--;
    exit_tracker--;
    if (max_indent != 0 && t_indent >= max_indent) {
        return;
    }
    recording = true;
    if (poll_index < POLL_SIZE) {
        thread_rec_t *rec = &rec_poll[poll_index++];
        rec->enter = false;
        rec->fnp = this_func;
        get_fname_fast(this_func, rec->fname);
        rec->caller = call_func;
        rec->count = 1;
        rec->real_time = time;
        thread_rec->insert(rec, NULL);
//        thread_rec_t *t = rec;
//        thread_rec_t *prev = (thread_rec_t *)rec->prev;
        bool rec_time = true;
#if 0
        if (prev && prev->fnp == rec->fnp && prev->caller == rec->caller && prev->enter == true) {
            for (int i =  0;i < 100 && prev;i++) {
                prev = (thread_rec_t *)prev->prev;
                if (prev && prev->fnp == rec->fnp && prev->caller == rec->caller && prev->enter == false) {
                    prev = (thread_rec_t *)prev->prev;
                    if (prev && prev->fnp == rec->fnp && prev->caller == rec->caller && prev->enter == true) {
                        rec = (thread_rec_t *)prev->next;
                        rec->real_time = prev->real_time;
                        rec->time = prev->time;
                        rec->count++;
                        poll_index = poll_index - 2;
                        t = (thread_rec_t *)t->prev->prev;
                        while (t->next) {
                            thread_rec->remove(t->next);
                        }
                        rec_time = false;
                        break;
                    }
                }
            }
        }
#endif
        if (rec_time)
            gettimeofday(&rec->time, NULL);
    } else {
    //    printf("poll_index = %d\n", ++poll_index);
    }
    recording = false;
}
}
